﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.CSharp;

namespace OrderAggregator.Formulas
{
    /// <summary>
    /// Formula manager
    /// </summary>
    public class FormulaManager
    {
        private readonly Dictionary<string, Func<decimal, decimal>> _delegatesByBody = new Dictionary<string, Func<decimal, decimal>>();

        private int _classCounter = 0;

        /// <summary>
        /// Default instance
        /// </summary>
        public readonly static FormulaManager Default = new FormulaManager();

        private FormulaManager()
        {
        }

        /// <summary>
        /// Precompile
        /// </summary>
        /// <param name="formulas"></param>
        public void PreCompile(IEnumerable<string> formulas)
        {
            formulas = formulas.Select(f => f.Trim());

            var newFormulas = formulas.Where(f => !_delegatesByBody.ContainsKey(f)).ToArray();

            if (newFormulas.Length == 0)
            {
                return;
            }

            var delegates = CreateFormulaDelegates(newFormulas).ToArray();

            for (int i = 0; i < newFormulas.Length; i++)
            {
                var formula = newFormulas[i];
                _delegatesByBody[formula] = delegates[i];
            }
            
        }

        /// <summary>
        /// Get or create formula delegates
        /// </summary>
        /// <param name="formula"></param>
        /// <returns></returns>
        public Func<decimal, decimal> GetFormulaDelegate(string formula)
        {
            Func<decimal, decimal> delegateInst;
            if (_delegatesByBody.TryGetValue(formula, out delegateInst))
            {
                return delegateInst;
            }
            return null;
        }

        /// <summary>
        /// Create formula delegates
        /// </summary>
        /// <param name="formulas"></param>
        /// <returns></returns>
        private IEnumerable<Func<decimal, decimal>> CreateFormulaDelegates(
            string[] formulas
            )
        {
            string className = "FormulasContainer" + _classCounter;
            _classCounter++;

            var methodNames = new List<string>();

            for (int methodCounter = 0; methodCounter < formulas.Length; methodCounter++)
            {
                string methodName = "Calculate" + methodCounter;
                methodNames.Add(methodName);
            }

            string code = GenerateFormulaCSCode(
                className,
                methodNames.ToArray(),
                formulas
                );

            

            var assembly = CompileAssembly(code);

            Type t = assembly.GetType(Namespace + "." + className);

            var functions = new List<Func<decimal, decimal>>();

            foreach (var methodName in methodNames)
            {
                var mi = t.GetMethod(methodName);

                var f = (Func<decimal, decimal>) Delegate.CreateDelegate(
                    typeof (Func<decimal, decimal>),
                    mi
                    );

                functions.Add(f);
            }

            return functions;
        }

        private const string Namespace = "OrderAggregator.Formulas.Implementations";


        private static string GenerateFormulaCSCode(
            string className,
            string[] methodNames,
            string[] bodies
            )
        {
            var sb = new StringBuilder();
            sb.AppendLine("using System;");
            sb.AppendLine("namespace " + Namespace);
            sb.AppendLine("{");
            sb.AppendFormat("public static class {0}", className);
            sb.AppendLine("{");

            for (int i = 0; i < methodNames.Length; i++ )
            {
                var methodName = methodNames[i];
                var body = bodies[i];

                sb.AppendFormat("public static decimal {0}(decimal value)", methodName);
                sb.AppendLine("{");
                sb.AppendLine(body);
                sb.AppendLine("}");
            }

            
            sb.AppendLine("}");
            sb.AppendLine("}");
            return sb.ToString();
        }

        private static Assembly CompileAssembly(string code)
        {
            var csProvider = new CSharpCodeProvider();
            
            var compileParams = new CompilerParameters();
            compileParams.GenerateInMemory = true;
            compileParams.GenerateExecutable = false;
            
            var compileResults = csProvider.CompileAssemblyFromSource(compileParams, code);

            if (compileResults.Errors.HasErrors)
            {
                var totalError = new StringBuilder();
                foreach (CompilerError error in compileResults.Errors)
                {
                    totalError.AppendLine(error.ErrorText);
                }

                throw new Exception(totalError.ToString());
            }

            return compileResults.CompiledAssembly;
        }
    }
}
